package com.chenwc.qiniukodo;

import com.alibaba.fastjson2.JSON;
import com.google.gson.Gson;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.*;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.storage.model.FetchRet;
import com.qiniu.storage.model.FileInfo;
import com.qiniu.storage.persistent.FileRecorder;
import com.qiniu.util.Auth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 七牛云对象存储工具类
 *
 * @author chenwc
 */
public class KodoUtil {

    private static final Logger logger = LoggerFactory.getLogger(KodoUtil.class);

    /**
     * AK
     */
    private static String accessKey;
    /**
     * SK
     */
    private static String secretKey;
    /**
     * 桶名
     */
    private static String bucket;

    public static void init(String userAccessKey, String userSecretKey, String userBucket) {
        accessKey = userAccessKey;
        secretKey = userSecretKey;
        bucket = userBucket;
    }

    /**
     * 获取简单上传的凭证
     *
     * @return 简单上传的凭证
     */
    public static String getUploadToken() {
        Auth auth = Auth.create(accessKey, secretKey);
        return auth.uploadToken(bucket);
    }


    /**
     * 获取覆盖上传的凭证
     *
     * @param key 文件名，文件名称同时是客户端上传代码中指定的文件名，两者必须一致
     * @return 覆盖上传的凭证
     */
    public static String getUploadTokenByKey(String key) {
        Auth auth = Auth.create(accessKey, secretKey);
        return auth.uploadToken(bucket, key);
    }

    /**
     * 上传文件
     *
     * @param filePath 文件路径
     * @param key      文件名.默认不指定key的情况下，以文件内容的hash值作为文件名
     */
    public static void uploadFile(String filePath, String key) {
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        // 指定分片上传版本
        cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;

        UploadManager uploadManager = new UploadManager(cfg);
        // token
        String upToken = getUploadToken();
        try {
            Response response = uploadManager.put(filePath, key, upToken);
            //解析上传成功的结果
            DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
            logger.info("上传文件成功: {}", JSON.toJSONString(putRet));
        } catch (QiniuException ex) {
            ex.printStackTrace();
            if (ex.response != null) {
                logger.error("上传文件失败: {}", ex.response);
                try {
                    String body = ex.response.toString();
                    logger.error("上传文件失败2: {}", body);
                } catch (Exception ignored) {
                }
            }
        }
    }

    /**
     * 上传文件
     *
     * @param uploadBytes 文件字节数组
     * @param key         文件名.默认不指定key的情况下，以文件内容的hash值作为文件名
     */
    public static void uploadFileByBytes(byte[] uploadBytes, String key) {
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        // 指定分片上传版本
        cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;

        UploadManager uploadManager = new UploadManager(cfg);
        // token
        String upToken = getUploadToken();
        try {
            Response response = uploadManager.put(uploadBytes, key, upToken);
            //解析上传成功的结果
            DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
            logger.info("上传文件成功: {}", JSON.toJSONString(putRet));
        } catch (QiniuException ex) {
            ex.printStackTrace();
            if (ex.response != null) {
                logger.error("上传文件失败: {}", ex.response);
                try {
                    String body = ex.response.toString();
                    logger.error("上传文件失败2: {}", body);
                } catch (Exception ignored) {
                }
            }
        }
    }

    /**
     * 断点续传上传
     *
     * @param filePath 文件路径
     * @param key      文件名.默认不指定key的情况下，以文件内容的hash值作为文件名
     */
    public static void resumeUploadFromBreakpoint(String filePath, String key){
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        // 指定分片上传版本
        cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;
        // 设置分片上传并发，1：采用同步上传；大于1：采用并发上传
        cfg.resumableUploadMaxConcurrentTaskCount = 2;
        // token
        String upToken = getUploadToken();
        // 本地临时目录
        String localTempDir = Paths.get(System.getenv("java.io.tmpdir"), bucket).toString();
        try {
            //设置断点续传文件进度保存目录
            FileRecorder fileRecorder = new FileRecorder(localTempDir);
            UploadManager uploadManager = new UploadManager(cfg, fileRecorder);
            try {
                Response response = uploadManager.put(filePath, key, upToken);
                //解析上传成功的结果
                DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
                logger.info("上传文件成功: {}", JSON.toJSONString(putRet));
            } catch (QiniuException ex) {
                ex.printStackTrace();
                if (ex.response != null) {
                    logger.error("上传文件失败: {}", ex.response);
                    try {
                        String body = ex.response.toString();
                        logger.error("上传文件失败2: {}", body);
                    } catch (Exception ignored) {
                    }
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
            logger.error("上传文件失败: {}", ex.getMessage());
        }
    }

    /**
     * 获取下载公开空间文件URL
     *
     * @param domainOfBucket 用户 bucket 绑定的下载域名
     * @param key            文件名
     * @return 下载文件的url
     */
    public static String downloadPublicZoneFileByUrl(String domainOfBucket, String key){
        String encodedFileName;
        try {
            encodedFileName = URLEncoder.encode(key, StandardCharsets.UTF_8.toString()).replace("+", "%20");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        String finalUrl = String.format("%s/%s", domainOfBucket, encodedFileName);
        logger.info("下载文件url: {}", finalUrl);
        return finalUrl;
    }

    /**
     * 获取下载公开空间文件URL
     *
     * @param domainOfBucket 用户 bucket 绑定的下载域名
     * @param key            七牛云中的文件名
     * @param attname 浏览器访问时指定下载文件名【可选】 默认情况下，如果在浏览器中访问一个资源URL，浏览器都会试图直接在浏览器中打开这个资源，例如一张图片。 如果希望浏览器的动作是下载而不是打开，可以给该资源URL添加参数 attname 来指定文件名
     * @return 下载文件的url
     */
    public static String downloadPublicZoneFileBySdk(String domainOfBucket, String key, String attname){
        DownloadUrl url = new DownloadUrl(domainOfBucket, true, key);
        // 配置 attname
        url.setAttname(attname);
        String urlString;
        try {
            urlString = url.buildURL();
        } catch (QiniuException e) {
            throw new RuntimeException(e);
        }
        logger.info("下载文件url: {}", urlString);
        return urlString;
    }

    /**
     * 获取下载私有空间文件URL
     *
     * @param domainOfBucket 用户 bucket 绑定的下载域名
     * @param key            文件名
     * @param expireInSeconds url过期时间
     * @return 下载文件的url
     */
    public static String downloadPrivateZoneFileByUrl(String domainOfBucket, String key, long expireInSeconds){
        String encodedFileName;
        try {
            encodedFileName = URLEncoder.encode(key, StandardCharsets.UTF_8.toString()).replace("+", "%20");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        String publicUrl = String.format("%s/%s", domainOfBucket, encodedFileName);
        Auth auth = Auth.create(accessKey, secretKey);
        String finalUrl = auth.privateDownloadUrl(publicUrl, expireInSeconds);
        logger.info("下载文件url: {}", finalUrl);
        return finalUrl;
    }

    /**
     * 获取下载私有空间文件URL
     *
     * @param domainOfBucket 用户 bucket 绑定的下载域名
     * @param key            七牛云中的文件名
     * @param attname 浏览器访问时指定下载文件名【可选】 默认情况下，如果在浏览器中访问一个资源URL，浏览器都会试图直接在浏览器中打开这个资源，例如一张图片。 如果希望浏览器的动作是下载而不是打开，可以给该资源URL添加参数 attname 来指定文件名
     * @param expireInSeconds url过期时间
     * @return 下载文件的url
     */
    public static String downloadPrivateZoneFileBySdk(String domainOfBucket, String key, String attname, long expireInSeconds){
        DownloadUrl url = new DownloadUrl(domainOfBucket, true, key);
        url.setAttname(attname) ;
        long deadline = System.currentTimeMillis()/1000 + expireInSeconds;
        Auth auth = Auth.create(accessKey, secretKey);
        String urlString;
        try {
            urlString = url.buildURL(auth, deadline);
        } catch (QiniuException e) {
            throw new RuntimeException(e);
        }
        logger.info("下载文件url: {}", urlString);
        return urlString;
    }

    /**
     * 获取文件信息
     * @param key 七牛云中的文件名
     * @return 文件信息
     */
    public static FileInfo getFileInfo(String key){
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        Auth auth = Auth.create(accessKey, secretKey);
        BucketManager bucketManager = new BucketManager(auth, cfg);
        FileInfo fileInfo = null;
        try {
            fileInfo = bucketManager.stat(bucket, key);
        } catch (QiniuException ex) {
            logger.error("获取文件信息失败: {}", ex.response.toString());
        }
        return fileInfo;
    }

    /**
     * 修改文件MineType
     * @param key 七牛云中的文件名
     * @param mineType 新的文件MineType
     */
    public static void modifyFileMineType(String key, String mineType){
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        Auth auth = Auth.create(accessKey, secretKey);
        BucketManager bucketManager = new BucketManager(auth, cfg);
        try {
            bucketManager.changeMime(bucket, key, mineType);
        } catch (QiniuException ex) {
            logger.error(ex.response.toString());
        }
    }

    /**
     * 重命名文件
     * @param key 七牛云中的文件名
     * @param newKey 新的文件名
     */
    public static void renameFile(String key, String newKey){
        moveFile(bucket, key, bucket, newKey);
    }

    /**
     * 移动文件
     * @param fromBucket 源bucket
     * @param fromKey 源文件名
     * @param toBucket 目标bucket
     * @param toKey 目标文件名
     */
    public static void moveFile(String fromBucket, String fromKey, String toBucket, String toKey){
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        Auth auth = Auth.create(accessKey, secretKey);
        BucketManager bucketManager = new BucketManager(auth, cfg);
        try {
            bucketManager.move(fromBucket, fromKey, toBucket, toKey);
        } catch (QiniuException ex) {
            logger.error(ex.response.toString());
        }
    }

    /**
     * 拷贝文件
     * @param fromBucket 源bucket
     * @param fromKey 源文件名
     * @param toBucket 目标bucket
     * @param toKey 目标文件名
     */
    public static void copyFile(String fromBucket, String fromKey, String toBucket, String toKey){
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        Auth auth = Auth.create(accessKey, secretKey);
        BucketManager bucketManager = new BucketManager(auth, cfg);
        try {
            bucketManager.copy(fromBucket, fromKey, toBucket, toKey);
        } catch (QiniuException ex) {
            logger.error(ex.response.toString());
        }
    }

    /**
     * 删除文件
     * @param key 七牛云中的文件名
     */
    public static void deleteFile(String key){
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        Auth auth = Auth.create(accessKey, secretKey);
        BucketManager bucketManager = new BucketManager(auth, cfg);
        try {
            bucketManager.delete(bucket, key);
        } catch (QiniuException ex) {
            logger.error(ex.response.toString());
        }
    }

    /**
     * 设置文件过期时间
     * @param key 七牛云中的文件名
     * @param deleteAfterDays 过期天数，该文件10天后删除
     */
    public static void updateDeleteAfterDays(String key, int deleteAfterDays){
        Configuration cfg = new Configuration(Region.autoRegion());
        Auth auth = Auth.create(accessKey, secretKey);
        BucketManager bucketManager = new BucketManager(auth, cfg);
        try {
            bucketManager.deleteAfterDays(bucket, key, deleteAfterDays);
        } catch (QiniuException ex) {
            logger.error(ex.response.toString());
        }
    }

    /**
     * 列举文件
     * @param prefix 文件名前缀
     * @return 文件信息列表
     */
    public static List<FileInfo> listFiles(String prefix){
        Configuration cfg = new Configuration(Region.autoRegion());
        Auth auth = Auth.create(accessKey, secretKey);
        BucketManager bucketManager = new BucketManager(auth, cfg);
        //每次迭代的长度限制，最大1000，推荐值 1000
        int limit = 1000;
        //指定目录分隔符，列出所有公共前缀（模拟列出目录效果）。缺省值为空字符串
        String delimiter = "";
        //列举空间文件列表
        BucketManager.FileListIterator fileListIterator = bucketManager.createFileListIterator(bucket, prefix, limit, delimiter);
        List<FileInfo> fileInfoList = new ArrayList<>();
        while (fileListIterator.hasNext()) {
            //处理获取的file list结果
            FileInfo[] items = fileListIterator.next();
            fileInfoList.addAll(Arrays.asList(items));
        }
        return fileInfoList;
    }

    /**
     * 抓取网络资源到七牛云
     * @param remoteSrcUrl 网络资源URL
     * @param key 七牛云中的文件名
     */
    public static FetchRet fetchRemoteFile(String remoteSrcUrl, String key){
        Configuration cfg = new Configuration(Region.autoRegion());
        Auth auth = Auth.create(accessKey, secretKey);
        BucketManager bucketManager = new BucketManager(auth, cfg);
        //抓取网络资源到空间
        try {
            return bucketManager.fetch(remoteSrcUrl, bucket, key);
        } catch (QiniuException ex) {
            logger.error(ex.response.toString());
        }
        return null;
    }
}
